# typed: true
# frozen_string_literal: true

require "migrator"
require "formulary"
require "cask/cask_loader"
require "cask/migrator"
require "descriptions"
require "cleanup"
require "description_cache_store"
require "cli/parser"
require "settings"
require "linuxbrew-core-migration"

module Homebrew
  module_function

  def auto_update_header(args:)
    @auto_update_header ||= begin
      ohai "Auto-updated Homebrew!" if args.auto_update?
      true
    end
  end

  sig { returns(CLI::Parser) }
  def update_report_args
    Homebrew::CLI::Parser.new do
      description <<~EOS
        The Ruby implementation of `brew update`. Never called manually.
      EOS
      switch "--auto-update", "--preinstall",
             description: "Run in 'auto-update' mode (faster, less output)."
      switch "-f", "--force",
             description: "Treat installed and updated formulae as if they are from " \
                          "the same taps and migrate them anyway."

      hide_from_man_page!
    end
  end

  def update_report
    return output_update_report if $stdout.tty?

    redirect_stdout($stderr) do
      output_update_report
    end
  end

  def output_update_report
    args = update_report_args.parse

    # Run `brew update` (again) if we've got a linuxbrew-core CoreTap
    if CoreTap.instance.installed? && CoreTap.instance.linuxbrew_core? &&
       ENV["HOMEBREW_LINUXBREW_CORE_MIGRATION"].blank?
      ohai "Re-running `brew update` for linuxbrew-core migration"

      if Homebrew::EnvConfig.core_git_remote != HOMEBREW_CORE_DEFAULT_GIT_REMOTE
        opoo <<~EOS
          HOMEBREW_CORE_GIT_REMOTE was set: #{Homebrew::EnvConfig.core_git_remote}.
          It has been unset for the migration.
          You may need to change this from a linuxbrew-core mirror to a homebrew-core one.

        EOS
      end
      ENV.delete("HOMEBREW_CORE_GIT_REMOTE")

      if Homebrew::EnvConfig.bottle_domain != HOMEBREW_BOTTLE_DEFAULT_DOMAIN
        opoo <<~EOS
          HOMEBREW_BOTTLE_DOMAIN was set: #{Homebrew::EnvConfig.bottle_domain}.
          It has been unset for the migration.
          You may need to change this from a Linuxbrew package mirror to a Homebrew one.

        EOS
      end
      ENV.delete("HOMEBREW_BOTTLE_DOMAIN")

      ENV["HOMEBREW_LINUXBREW_CORE_MIGRATION"] = "1"
      FileUtils.rm_f HOMEBREW_LOCKS/"update"

      update_args = []
      update_args << "--auto-update" if args.auto_update?
      update_args << "--force" if args.force?
      exec HOMEBREW_BREW_FILE, "update", *update_args
    end

    if ENV["HOMEBREW_ADDITIONAL_GOOGLE_ANALYTICS_ID"].present?
      opoo "HOMEBREW_ADDITIONAL_GOOGLE_ANALYTICS_ID is now a no-op so can be unset."
      puts "All Homebrew Google Analytics code and data was destroyed."
    end

    if ENV["HOMEBREW_NO_GOOGLE_ANALYTICS"].present?
      opoo "HOMEBREW_NO_GOOGLE_ANALYTICS is now a no-op so can be unset."
      puts "All Homebrew Google Analytics code and data was destroyed."
    end

    unless args.quiet?
      analytics_message
      donation_message
      install_from_api_message
    end

    tap_or_untap_core_taps_if_necessary

    updated = false
    new_tag = nil

    initial_revision = ENV["HOMEBREW_UPDATE_BEFORE"].to_s
    current_revision = ENV["HOMEBREW_UPDATE_AFTER"].to_s
    odie "update-report should not be called directly!" if initial_revision.empty? || current_revision.empty?

    if initial_revision != current_revision
      auto_update_header args: args

      updated = true

      old_tag = Settings.read "latesttag"

      new_tag = Utils.popen_read(
        "git", "-C", HOMEBREW_REPOSITORY, "tag", "--list", "--sort=-version:refname", "*.*"
      ).lines.first.chomp

      Settings.write "latesttag", new_tag if new_tag != old_tag

      if new_tag == old_tag
        ohai "Updated Homebrew from #{shorten_revision(initial_revision)} to #{shorten_revision(current_revision)}."
      elsif old_tag.blank?
        ohai "Updated Homebrew from #{shorten_revision(initial_revision)} " \
             "to #{new_tag} (#{shorten_revision(current_revision)})."
      else
        ohai "Updated Homebrew from #{old_tag} (#{shorten_revision(initial_revision)}) " \
             "to #{new_tag} (#{shorten_revision(current_revision)})."
      end
    end

    # Check if we can parse the JSON and do any Ruby-side follow-up.
    unless Homebrew::EnvConfig.no_install_from_api?
      Homebrew::API::Formula.write_names_and_aliases
      Homebrew::API::Cask.write_names
    end

    Homebrew.failed = true if ENV["HOMEBREW_UPDATE_FAILED"]
    return if Homebrew::EnvConfig.disable_load_formula?

    migrate_gcc_dependents_if_needed

    hub = ReporterHub.new

    updated_taps = []
    Tap.each do |tap|
      next if !tap.git? || tap.git_repo.origin_url.nil?
      next if (tap.core_tap? || tap.core_cask_tap?) && !Homebrew::EnvConfig.no_install_from_api?

      if ENV["HOMEBREW_MIGRATE_LINUXBREW_FORMULAE"].present? && tap.core_tap? &&
         Settings.read("linuxbrewmigrated") != "true"
        ohai "Migrating formulae from linuxbrew-core to homebrew-core"

        LINUXBREW_CORE_MIGRATION_LIST.each do |name|
          begin
            formula = Formula[name]
          rescue FormulaUnavailableError
            next
          end
          next unless formula.any_version_installed?

          keg = formula.installed_kegs.last
          tab = Tab.for_keg(keg)
          # force a `brew upgrade` from the linuxbrew-core version to the homebrew-core version (even if lower)
          tab.source["versions"]["version_scheme"] = -1
          tab.write
        end

        Settings.write "linuxbrewmigrated", true
      end

      begin
        reporter = Reporter.new(tap)
      rescue Reporter::ReporterRevisionUnsetError => e
        onoe "#{e.message}\n#{e.backtrace&.join("\n")}" if Homebrew::EnvConfig.developer?
        next
      end
      if reporter.updated?
        updated_taps << tap.name
        hub.add(reporter, auto_update: args.auto_update?)
      end
    end

    # If we're installing from the API: we cannot use Git to check for #
    # differences in packages so instead use {formula,cask}_names.txt to do so.
    # The first time this runs: we won't yet have a base state
    # ({formula,cask}_names.before.txt) to compare against so we don't output a
    # anything and just copy the files for next time.
    unless Homebrew::EnvConfig.no_install_from_api?
      api_cache = Homebrew::API::HOMEBREW_CACHE_API
      core_tap = CoreTap.instance
      cask_tap = CoreCaskTap.instance
      [
        [:formula, core_tap, core_tap.formula_dir],
        [:cask,    cask_tap, cask_tap.cask_dir],
      ].each do |type, tap, dir|
        names_txt = api_cache/"#{type}_names.txt"
        next unless names_txt.exist?

        names_before_txt = api_cache/"#{type}_names.before.txt"
        if names_before_txt.exist?
          reporter = Reporter.new(
            tap,
            api_names_txt:        names_txt,
            api_names_before_txt: names_before_txt,
            api_dir_prefix:       dir,
          )
          if reporter.updated?
            updated_taps << tap.name
            hub.add(reporter, auto_update: args.auto_update?)
          end
        else
          FileUtils.cp names_txt, names_before_txt
        end
      end
    end

    unless updated_taps.empty?
      auto_update_header args: args
      puts "Updated #{Utils.pluralize("tap", updated_taps.count, include_count: true)} (#{updated_taps.to_sentence})."
      updated = true
    end

    if updated
      if hub.empty?
        puts no_changes_message unless args.quiet?
      else
        if ENV.fetch("HOMEBREW_UPDATE_REPORT_ONLY_INSTALLED", false)
          opoo "HOMEBREW_UPDATE_REPORT_ONLY_INSTALLED is now the default behaviour, " \
               "so you can unset it from your environment."
        end

        hub.dump(auto_update: args.auto_update?) unless args.quiet?
        hub.reporters.each(&:migrate_tap_migration)
        hub.reporters.each(&:migrate_cask_rename)
        hub.reporters.each { |r| r.migrate_formula_rename(force: args.force?, verbose: args.verbose?) }

        CacheStoreDatabase.use(:descriptions) do |db|
          DescriptionCacheStore.new(db)
                               .update_from_report!(hub)
        end
        CacheStoreDatabase.use(:cask_descriptions) do |db|
          CaskDescriptionCacheStore.new(db)
                                   .update_from_report!(hub)
        end
      end
      puts if args.auto_update?
    elsif !args.auto_update? && !ENV["HOMEBREW_UPDATE_FAILED"] && !ENV["HOMEBREW_MIGRATE_LINUXBREW_FORMULAE"]
      puts "Already up-to-date." unless args.quiet?
    end

    Commands.rebuild_commands_completion_list
    link_completions_manpages_and_docs
    Tap.each(&:link_completions_and_manpages)

    failed_fetch_dirs = ENV["HOMEBREW_MISSING_REMOTE_REF_DIRS"]&.split("\n")
    if failed_fetch_dirs.present?
      failed_fetch_taps = failed_fetch_dirs.map { |dir| Tap.from_path(dir) }

      ofail <<~EOS
        Some taps failed to update!
        The following taps can not read their remote branches:
          #{failed_fetch_taps.join("\n  ")}
        This is happening because the remote branch was renamed or deleted.
        Reset taps to point to the correct remote branches by running `brew tap --repair`
      EOS
    end

    return if new_tag.blank? || new_tag == old_tag || args.quiet?

    puts

    new_major_version, new_minor_version, new_patch_version = new_tag.split(".").map(&:to_i)
    old_major_version, old_minor_version = (old_tag.split(".")[0, 2]).map(&:to_i) if old_tag.present?
    if old_tag.blank? || new_major_version > old_major_version \
        || new_minor_version > old_minor_version
      puts <<~EOS
        The #{new_major_version}.#{new_minor_version}.0 release notes are available on the Homebrew Blog:
          #{Formatter.url("https://brew.sh/blog/#{new_major_version}.#{new_minor_version}.0")}
      EOS
    end

    return if new_patch_version.zero?

    puts <<~EOS
      The #{new_tag} changelog can be found at:
        #{Formatter.url("https://github.com/Homebrew/brew/releases/tag/#{new_tag}")}
    EOS
  end

  def no_changes_message
    "No changes to formulae or casks."
  end

  def shorten_revision(revision)
    Utils.popen_read("git", "-C", HOMEBREW_REPOSITORY, "rev-parse", "--short", revision).chomp
  end

  def tap_or_untap_core_taps_if_necessary
    return if ENV["HOMEBREW_UPDATE_TEST"]

    if Homebrew::EnvConfig.no_install_from_api?
      return if Homebrew::EnvConfig.automatically_set_no_install_from_api?
      return if CoreTap.instance.installed?

      CoreTap.ensure_installed!
      revision = CoreTap.instance.git_head
      ENV["HOMEBREW_UPDATE_BEFORE_HOMEBREW_HOMEBREW_CORE"] = revision
      ENV["HOMEBREW_UPDATE_AFTER_HOMEBREW_HOMEBREW_CORE"] = revision
    else
      return if Homebrew::EnvConfig.developer? || ENV["HOMEBREW_DEV_CMD_RUN"]
      return if ENV["HOMEBREW_GITHUB_HOSTED_RUNNER"] || ENV["GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED"]
      return if (HOMEBREW_PREFIX/".homebrewdocker").exist?

      tap_output_header_printed = T.let(false, T::Boolean)
      [CoreTap.instance, CoreCaskTap.instance].each do |tap|
        next unless tap.installed?

        if tap.git_branch == "master" &&
           (Date.parse(tap.git_repo.last_commit_date) <= Date.today.prev_month)
          ohai "#{tap.name} is old and unneeded, untapping to save space..."
          tap.uninstall
        else
          unless tap_output_header_printed
            puts "Installing from the API is now the default behaviour!"
            puts "You can save space and time by running:"
            tap_output_header_printed = true
          end
          puts "  brew untap #{tap.name}"
        end
      end
    end
  end

  def link_completions_manpages_and_docs(repository = HOMEBREW_REPOSITORY)
    command = "brew update"
    Utils::Link.link_completions(repository, command)
    Utils::Link.link_manpages(repository, command)
    Utils::Link.link_docs(repository, command)
  rescue => e
    ofail <<~EOS
      Failed to link all completions, docs and manpages:
        #{e}
    EOS
  end

  def migrate_gcc_dependents_if_needed
    # do nothing
  end

  def analytics_message
    return if Utils::Analytics.messages_displayed?
    return if Utils::Analytics.no_message_output?

    if Utils::Analytics.disabled? && !Utils::Analytics.influx_message_displayed?
      ohai "Homebrew's analytics have entirely moved to our InfluxDB instance in the EU."
      puts "We gather less data than before and have destroyed all Google Analytics data:"
      puts "  #{Formatter.url("https://docs.brew.sh/Analytics")}#{Tty.reset}"
      puts "Please reconsider re-enabling analytics to help our volunteer maintainers with:"
      puts "  brew analytics on"
    elsif !Utils::Analytics.disabled?
      ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"] = "1"
      # Use the shell's audible bell.
      print "\a"

      # Use an extra newline and bold to avoid this being missed.
      ohai "Homebrew collects anonymous analytics."
      puts <<~EOS
        #{Tty.bold}Read the analytics documentation (and how to opt-out) here:
          #{Formatter.url("https://docs.brew.sh/Analytics")}#{Tty.reset}
        No analytics have been recorded yet (nor will be during this `brew` run).

      EOS
    end

    # Consider the messages possibly missed if not a TTY.
    Utils::Analytics.messages_displayed! if $stdout.tty?
  end

  def donation_message
    return if Settings.read("donationmessage") == "true"

    ohai "Homebrew is run entirely by unpaid volunteers. Please consider donating:"
    puts "  #{Formatter.url("https://github.com/Homebrew/brew#donations")}\n\n"

    # Consider the message possibly missed if not a TTY.
    Settings.write "donationmessage", true if $stdout.tty?
  end

  def install_from_api_message
    return if Settings.read("installfromapimessage") == "true"

    api_auto_update_secs = Homebrew::EnvConfig.api_auto_update_secs.to_i
    api_auto_update_secs_default = Homebrew::EnvConfig::ENVS.fetch(:HOMEBREW_API_AUTO_UPDATE_SECS).fetch(:default)
    auto_update_secs_set = api_auto_update_secs.positive? && api_auto_update_secs != api_auto_update_secs_default
    no_auto_update_set = Homebrew::EnvConfig.no_auto_update? &&
                         !ENV["HOMEBREW_GITHUB_HOSTED_RUNNER"] &&
                         !ENV["GITHUB_ACTIONS_HOMEBREW_SELF_HOSTED"]
    no_install_from_api_set = Homebrew::EnvConfig.no_install_from_api? &&
                              !Homebrew::EnvConfig.automatically_set_no_install_from_api?
    return if !no_auto_update_set && !no_install_from_api_set && !auto_update_secs_set

    ohai "You have set:"
    puts "  HOMEBREW_NO_AUTO_UPDATE" if no_auto_update_set
    puts "  HOMEBREW_API_AUTO_UPDATE_SECS" if auto_update_secs_set
    puts "  HOMEBREW_NO_INSTALL_FROM_API" if no_install_from_api_set
    puts "but we have dramatically sped up and fixed many bugs in the way we do Homebrew updates since."
    puts "Please consider unsetting these and tweaking the values based on the new behaviour."
    puts "\n\n"

    # Consider the message possibly missed if not a TTY.
    Settings.write "installfromapimessage", true if $stdout.tty?
  end
end

require "extend/os/cmd/update-report"

class Reporter
  class ReporterRevisionUnsetError < RuntimeError
    def initialize(var_name)
      super "#{var_name} is unset!"
    end
  end

  def initialize(tap, api_names_txt: nil, api_names_before_txt: nil, api_dir_prefix: nil)
    @tap = tap

    # This is slightly involved/weird but all the #report logic is shared so it's worth it.
    if installed_from_api?(api_names_txt, api_names_before_txt, api_dir_prefix)
      @api_names_txt = api_names_txt
      @api_names_before_txt = api_names_before_txt
      @api_dir_prefix = api_dir_prefix
    else
      initial_revision_var = "HOMEBREW_UPDATE_BEFORE#{tap.repo_var}"
      @initial_revision = ENV[initial_revision_var].to_s
      raise ReporterRevisionUnsetError, initial_revision_var if @initial_revision.empty?

      current_revision_var = "HOMEBREW_UPDATE_AFTER#{tap.repo_var}"
      @current_revision = ENV[current_revision_var].to_s
      raise ReporterRevisionUnsetError, current_revision_var if @current_revision.empty?
    end
  end

  def report(auto_update: false)
    return @report if @report

    @report = Hash.new { |h, k| h[k] = [] }
    return @report unless updated?

    diff.each_line do |line|
      status, *paths = line.split
      src = Pathname.new paths.first
      dst = Pathname.new paths.last

      next if dst.extname != ".rb"

      if paths.any? { |p| tap.cask_file?(p) }
        case status
        when "A"
          # Have a dedicated report array for new casks.
          @report[:AC] << tap.formula_file_to_name(src)
        when "D"
          # Have a dedicated report array for deleted casks.
          @report[:DC] << tap.formula_file_to_name(src)
        when "M"
          # Report updated casks
          @report[:MC] << tap.formula_file_to_name(src)
        when /^R\d{0,3}/
          src_full_name = tap.formula_file_to_name(src)
          dst_full_name = tap.formula_file_to_name(dst)
          # Don't report formulae that are moved within a tap but not renamed
          next if src_full_name == dst_full_name

          @report[:DC] << src_full_name
          @report[:AC] << dst_full_name
        end
      end

      next unless paths.any? { |p| tap.formula_file?(p) }

      case status
      when "A", "D"
        full_name = tap.formula_file_to_name(src)
        name = full_name.split("/").last
        new_tap = tap.tap_migrations[name]
        @report[status.to_sym] << full_name unless new_tap
      when "M"
        name = tap.formula_file_to_name(src)

        @report[:M] << name
      when /^R\d{0,3}/
        src_full_name = tap.formula_file_to_name(src)
        dst_full_name = tap.formula_file_to_name(dst)
        # Don't report formulae that are moved within a tap but not renamed
        next if src_full_name == dst_full_name

        @report[:D] << src_full_name
        @report[:A] << dst_full_name
      end
    end

    renamed_casks = Set.new
    @report[:DC].each do |old_full_name|
      old_name = old_full_name.split("/").last
      new_name = tap.cask_renames[old_name]
      next unless new_name

      new_full_name = if tap.core_cask_tap?
        new_name
      else
        "#{tap}/#{new_name}"
      end

      renamed_casks << [old_full_name, new_full_name] if @report[:AC].include?(new_full_name)
    end

    @report[:AC].each do |new_full_name|
      new_name = new_full_name.split("/").last
      old_name = tap.cask_renames.key(new_name)
      next unless old_name

      old_full_name = if tap.core_cask_tap?
        old_name
      else
        "#{tap}/#{old_name}"
      end

      renamed_casks << [old_full_name, new_full_name]
    end

    if renamed_casks.any?
      @report[:AC] -= renamed_casks.map(&:last)
      @report[:DC] -= renamed_casks.map(&:first)
      @report[:RC] = renamed_casks.to_a
    end

    renamed_formulae = Set.new
    @report[:D].each do |old_full_name|
      old_name = old_full_name.split("/").last
      new_name = tap.formula_renames[old_name]
      next unless new_name

      new_full_name = if tap.core_tap?
        new_name
      else
        "#{tap}/#{new_name}"
      end

      renamed_formulae << [old_full_name, new_full_name] if @report[:A].include? new_full_name
    end

    @report[:A].each do |new_full_name|
      new_name = new_full_name.split("/").last
      old_name = tap.formula_renames.key(new_name)
      next unless old_name

      old_full_name = if tap.core_tap?
        old_name
      else
        "#{tap}/#{old_name}"
      end

      renamed_formulae << [old_full_name, new_full_name]
    end

    if renamed_formulae.any?
      @report[:A] -= renamed_formulae.map(&:last)
      @report[:D] -= renamed_formulae.map(&:first)
      @report[:R] = renamed_formulae.to_a
    end

    # If any formulae/casks are marked as added and deleted, remove them from
    # the report as we've not detected things correctly.
    if (added_and_deleted_formulae = (@report[:A] & @report[:D]).presence)
      @report[:A] -= added_and_deleted_formulae
      @report[:D] -= added_and_deleted_formulae
    end
    if (added_and_deleted_casks = (@report[:AC] & @report[:DC]).presence)
      @report[:AC] -= added_and_deleted_casks
      @report[:DC] -= added_and_deleted_casks
    end

    @report
  end

  def updated?
    if installed_from_api?
      diff.present?
    else
      initial_revision != current_revision
    end
  end

  def migrate_tap_migration
    (report[:D] + report[:DC]).each do |full_name|
      name = full_name.split("/").last
      new_tap_name = tap.tap_migrations[name]
      next if new_tap_name.nil? # skip if not in tap_migrations list.

      new_tap_user, new_tap_repo, new_tap_new_name = new_tap_name.split("/")
      new_name = if new_tap_new_name
        new_full_name = new_tap_new_name
        new_tap_name = "#{new_tap_user}/#{new_tap_repo}"
        new_tap_new_name
      else
        new_full_name = "#{new_tap_name}/#{name}"
        name
      end

      # This means it is a cask
      if report[:DC].include? full_name
        next unless (HOMEBREW_PREFIX/"Caskroom"/new_name).exist?

        new_tap = Tap.fetch(new_tap_name)
        new_tap.ensure_installed!
        ohai "#{name} has been moved to Homebrew.", <<~EOS
          To uninstall the cask, run:
            brew uninstall --cask --force #{name}
        EOS
        next if (HOMEBREW_CELLAR/new_name.split("/").last).directory?

        ohai "Installing #{new_name}..."
        system HOMEBREW_BREW_FILE, "install", new_full_name
        begin
          unless Formulary.factory(new_full_name).keg_only?
            system HOMEBREW_BREW_FILE, "link", new_full_name, "--overwrite"
          end
        rescue Exception => e # rubocop:disable Lint/RescueException
          onoe "#{e.message}\n#{e.backtrace&.join("\n")}" if Homebrew::EnvConfig.developer?
        end
        next
      end

      next unless (dir = HOMEBREW_CELLAR/name).exist? # skip if formula is not installed.

      tabs = dir.subdirs.map { |d| Tab.for_keg(Keg.new(d)) }
      next if tabs.first.tap != tap # skip if installed formula is not from this tap.

      new_tap = Tap.fetch(new_tap_name)
      # For formulae migrated to cask: Auto-install cask or provide install instructions.
      if new_tap_name.start_with?("homebrew/cask")
        if new_tap.installed? && (HOMEBREW_PREFIX/"Caskroom").directory?
          ohai "#{name} has been moved to Homebrew Cask."
          ohai "brew unlink #{name}"
          system HOMEBREW_BREW_FILE, "unlink", name
          ohai "brew cleanup"
          system HOMEBREW_BREW_FILE, "cleanup"
          ohai "brew install --cask #{new_name}"
          system HOMEBREW_BREW_FILE, "install", "--cask", new_name
          ohai <<~EOS
            #{name} has been moved to Homebrew Cask.
            The existing keg has been unlinked.
            Please uninstall the formula when convenient by running:
              brew uninstall --force #{name}
          EOS
        else
          ohai "#{name} has been moved to Homebrew Cask.", <<~EOS
            To uninstall the formula and install the cask, run:
              brew uninstall --force #{name}
              brew tap #{new_tap_name}
              brew install --cask #{new_name}
          EOS
        end
      else
        new_tap.ensure_installed!
        # update tap for each Tab
        tabs.each { |tab| tab.tap = new_tap }
        tabs.each(&:write)
      end
    end
  end

  def migrate_cask_rename
    Cask::Caskroom.casks.each do |cask|
      Cask::Migrator.migrate_if_needed(cask)
    end
  end

  def migrate_formula_rename(force:, verbose:)
    Formula.installed.each do |formula|
      next unless Migrator.needs_migration?(formula)

      oldnames_to_migrate = formula.oldnames.select do |oldname|
        oldname_rack = HOMEBREW_CELLAR/oldname
        next false unless oldname_rack.exist?

        if oldname_rack.subdirs.empty?
          oldname_rack.rmdir_if_possible
          next false
        end

        true
      end
      next if oldnames_to_migrate.empty?

      Migrator.migrate_if_needed(formula, force: force)
    end
  end

  private

  attr_reader :tap, :initial_revision, :current_revision, :api_names_txt, :api_names_before_txt, :api_dir_prefix

  def installed_from_api?(api_names_txt = @api_names_txt, api_names_before_txt = @api_names_before_txt,
                          api_dir_prefix = @api_dir_prefix)
    !api_names_txt.nil? && !api_names_before_txt.nil? && !api_dir_prefix.nil?
  end

  def diff
    @diff ||= if installed_from_api?
      # Hack `git diff` output with regexes to look like `git diff-tree` output.
      # Yes, I know this is a bit filthy but it saves duplicating the #report logic.
      diff_output = Utils.popen_read("git", "diff", "--no-ext-diff", api_names_before_txt, api_names_txt)
      header_regex = /^(---|\+\+\+) /.freeze
      add_delete_characters = ["+", "-"].freeze

      diff_output.lines.map do |line|
        next if line.match?(header_regex)
        next unless add_delete_characters.include?(line[0])

        line.sub(/^\+/, "A #{api_dir_prefix.basename}/")
            .sub(/^-/,  "D #{api_dir_prefix.basename}/")
            .sub(/$/,   ".rb")
            .chomp
      end.compact.join("\n")
    else
      Utils.popen_read(
        "git", "-C", tap.path, "diff-tree", "-r", "--name-status", "--diff-filter=AMDR",
        "-M85%", initial_revision, current_revision
      )
    end
  end
end

class ReporterHub
  attr_reader :reporters

  sig { void }
  def initialize
    @hash = {}
    @reporters = []
  end

  def select_formula_or_cask(key)
    @hash.fetch(key, [])
  end

  def add(reporter, auto_update: false)
    @reporters << reporter
    report = reporter.report(auto_update: auto_update).delete_if { |_k, v| v.empty? }
    @hash.update(report) { |_key, oldval, newval| oldval.concat(newval) }
  end

  def empty?
    @hash.empty?
  end

  def dump(auto_update: false)
    report_all = ENV["HOMEBREW_UPDATE_REPORT_ALL_FORMULAE"].present?
    if report_all && !Homebrew::EnvConfig.no_install_from_api?
      odisabled "HOMEBREW_UPDATE_REPORT_ALL_FORMULAE"
      opoo "This will not report all formulae because Homebrew cannot get this data from the API."
      report_all = false
    end

    unless Homebrew::EnvConfig.no_update_report_new?
      dump_new_formula_report
      dump_new_cask_report
    end

    if report_all
      dump_renamed_formula_report
      dump_renamed_cask_report
    end

    dump_deleted_formula_report(report_all)
    dump_deleted_cask_report(report_all)

    outdated_formulae = []
    outdated_casks = []

    if report_all
      if auto_update
        if (changed_formulae = select_formula_or_cask(:M).count) && changed_formulae.positive?
          ohai "Modified Formulae",
               "Modified #{Utils.pluralize("formula", changed_formulae, plural: "e", include_count: true)}."
        end

        if (changed_casks = select_formula_or_cask(:MC).count) && changed_casks.positive?
          ohai "Modified Casks", "Modified #{Utils.pluralize("cask", changed_casks, include_count: true)}."
        end
      else
        dump_modified_formula_report
        dump_modified_cask_report
      end
    else
      outdated_formulae = Formula.installed.select(&:outdated?).map(&:name)
      outdated_casks = Cask::Caskroom.casks.select(&:outdated?).map(&:token)
      unless auto_update
        output_dump_formula_or_cask_report "Outdated Formulae", outdated_formulae
        output_dump_formula_or_cask_report "Outdated Casks", outdated_casks
      end
    end

    return if outdated_formulae.blank? && outdated_casks.blank?

    outdated_formulae = outdated_formulae.count
    outdated_casks = outdated_casks.count

    update_pronoun = if (outdated_formulae + outdated_casks) == 1
      "it"
    else
      "them"
    end

    msg = ""

    if outdated_formulae.positive?
      noun = Utils.pluralize("formula", outdated_formulae, plural: "e")
      msg += "#{Tty.bold}#{outdated_formulae}#{Tty.reset} outdated #{noun}"
    end

    if outdated_casks.positive?
      msg += " and " if msg.present?
      msg += "#{Tty.bold}#{outdated_casks}#{Tty.reset} outdated #{Utils.pluralize("cask", outdated_casks)}"
    end

    return if msg.blank?

    puts
    puts "You have #{msg} installed."
    # If we're auto-updating, don't need to suggest commands that we're perhaps
    # already running.
    return if auto_update

    puts <<~EOS
      You can upgrade #{update_pronoun} with #{Tty.bold}brew upgrade#{Tty.reset}
      or list #{update_pronoun} with #{Tty.bold}brew outdated#{Tty.reset}.
    EOS
  end

  private

  def dump_new_formula_report
    formulae = select_formula_or_cask(:A).sort.reject { |name| installed?(name) }

    output_dump_formula_or_cask_report "New Formulae", formulae
  end

  def dump_new_cask_report
    casks = select_formula_or_cask(:AC).sort.map do |name|
      name.split("/").last unless cask_installed?(name)
    end.compact

    output_dump_formula_or_cask_report "New Casks", casks
  end

  def dump_renamed_formula_report
    formulae = select_formula_or_cask(:R).sort.map do |name, new_name|
      name = pretty_installed(name) if installed?(name)
      new_name = pretty_installed(new_name) if installed?(new_name)
      "#{name} -> #{new_name}"
    end

    output_dump_formula_or_cask_report "Renamed Formulae", formulae
  end

  def dump_renamed_cask_report
    casks = select_formula_or_cask(:RC).sort.map do |name, new_name|
      name = pretty_installed(name) if installed?(name)
      new_name = pretty_installed(new_name) if installed?(new_name)
      "#{name} -> #{new_name}"
    end

    output_dump_formula_or_cask_report "Renamed Casks", casks
  end

  def dump_deleted_formula_report(report_all)
    formulae = select_formula_or_cask(:D).sort.map do |name|
      if installed?(name)
        pretty_uninstalled(name)
      elsif report_all
        name
      end
    end.compact

    title = if report_all
      "Deleted Formulae"
    else
      "Deleted Installed Formulae"
    end
    output_dump_formula_or_cask_report title, formulae
  end

  def dump_deleted_cask_report(report_all)
    casks = select_formula_or_cask(:DC).sort.map do |name|
      name = name.split("/").last
      if cask_installed?(name)
        pretty_uninstalled(name)
      elsif report_all
        name
      end
    end.compact

    title = if report_all
      "Deleted Casks"
    else
      "Deleted Installed Casks"
    end
    output_dump_formula_or_cask_report title, casks
  end

  def dump_modified_formula_report
    formulae = select_formula_or_cask(:M).sort.map do |name|
      if installed?(name)
        if outdated?(name)
          pretty_outdated(name)
        else
          pretty_installed(name)
        end
      else
        name
      end
    end

    output_dump_formula_or_cask_report "Modified Formulae", formulae
  end

  def dump_modified_cask_report
    casks = select_formula_or_cask(:MC).sort.map do |name|
      name = name.split("/").last
      if cask_installed?(name)
        if cask_outdated?(name)
          pretty_outdated(name)
        else
          pretty_installed(name)
        end
      else
        name
      end
    end

    output_dump_formula_or_cask_report "Modified Casks", casks
  end

  def output_dump_formula_or_cask_report(title, formulae_or_casks)
    return if formulae_or_casks.blank?

    ohai title, Formatter.columns(formulae_or_casks.sort)
  end

  def installed?(formula)
    (HOMEBREW_CELLAR/formula.split("/").last).directory?
  end

  def outdated?(formula)
    Formula[formula].outdated?
  rescue FormulaUnavailableError
    false
  end

  def cask_installed?(cask)
    (Cask::Caskroom.path/cask).directory?
  end

  def cask_outdated?(cask)
    Cask::CaskLoader.load(cask).outdated?
  rescue Cask::CaskError
    false
  end
end
